﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using JigLibX.Physics;
using JigLibX.Geometry;
using JigLibX.Collision;
using JigLibX.Math;


namespace LloydGame
{
    public class Camera : GameComponent
    {
        #region Class Fields
        private Vector3 m_position;
        private Vector3 m_lookAt;
        private Vector3 m_rotation;

        protected Matrix m_projectionMatrix;
        protected Matrix m_viewMatrix;
        protected Matrix m_viewProj;

        private GameObject m_targetObject;

        private float m_correction_factor = 0.002f;
        private float m_altSpeed = 15.0f;
        private float m_speed = 150.0f; // distance unit / second
        private float m_rotationSpeed = 0.003f;
        private float m_rotationRadius;
        private float m_farClip = 1000.0f;
        private float m_fov;
        private float m_aspectRatio;

        private cameraState m_state;
        private Vector2 m_prevMousePosition;

        public enum cameraState { FREE, FOLLOW, ROTATE_AROUND, EDIT_MODE };
        public enum cameraEnum { LEFT, RIGHT, FORWARD, BACK, UP, DOWN };
        #endregion

        #region Properties
        public Vector3 position
        {
            get
            {
                return m_position;
            }
        }

        public float aspectRatio
        {
            get
            {
                return m_aspectRatio;
            }
        }

        public float fov
        {
            get
            {
                return m_fov;
            }
        }

        public float farClip
        {
            get
            {
                return m_farClip;
            }
            set
            {
                m_farClip = value;

                m_projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
                    m_fov,
                    m_aspectRatio,
                    1.0f, // near clipping plane
                    m_farClip); // far clipping plane

                m_viewProj = m_viewMatrix * m_projectionMatrix;
            }
        }

        public Matrix view
        {
            get
            {
                return m_viewMatrix;
            }
        }

        public Matrix proj
        {
            get
            {
                return m_projectionMatrix;
            }
        }

        public Matrix viewProj
        {
            get
            {
                return m_viewProj;
            }
        }
        #endregion

        public Camera(Game p_game,
                      Vector3 p_position,
                      Vector3 p_lookAt,
                      cameraState p_state,
                      GameObject p_targetObj) : base(p_game)
        {
            m_aspectRatio = ((Game1)Game).graphics.GraphicsDevice.Viewport.AspectRatio;
            setupCamera(p_position, p_lookAt, p_state, p_targetObj);
        }
        
        public void setupCamera(Vector3 p_position, 
                                Vector3 p_lookAt, 
                                cameraState p_state,
                                GameObject p_targetObj)
        {
            m_state = p_state;
            m_targetObject = p_targetObj;
            m_rotation = Vector3.Zero;
            
            switch (m_state)
            {
                case cameraState.FREE:
                    m_position = p_position;
                    m_lookAt = p_lookAt;
                    //Sets the mouse position to the middle of the view area
                    Mouse.SetPosition(((Game1)Game).graphics.GraphicsDevice.Viewport.Width / 2, ((Game1)Game).graphics.GraphicsDevice.Viewport.Height / 2);
                    break;
                case cameraState.FOLLOW:
                    m_position = m_targetObject.position + Vector3.Transform(
                                 Vector3.Left * m_targetObject.gameModels[0].model.Meshes[0].BoundingSphere.Radius * 3,
                                 Matrix.CreateRotationZ(MathHelper.ToRadians(-10)) * m_targetObject.body.Orientation);
                    m_lookAt = m_targetObject.position;
                    break;

                case cameraState.ROTATE_AROUND:
                    
                    m_rotationRadius = (m_targetObject.gameModels)[0].model.Meshes[0].BoundingSphere.Radius * 3 * m_targetObject.scale.X;
                    Matrix rot_ = Matrix.CreateFromYawPitchRoll(m_rotation.Y, m_rotation.X, 0);                    
                    m_position = m_targetObject.position + Vector3.Transform(Vector3.Backward * m_rotationRadius, rot_);                    
                    m_lookAt = m_targetObject.position;
                    break;
                case cameraState.EDIT_MODE:
                    m_position = p_position;
                    m_lookAt = p_lookAt;
                    break;
            }

            m_fov = MathHelper.ToRadians(45.0f);

            m_viewMatrix = Matrix.CreateLookAt(m_position, m_lookAt, Vector3.Up);
            m_projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
                m_fov,
                ((Game1)Game).graphics.GraphicsDevice.Viewport.AspectRatio,
                1.0f, // near clipping plane
                m_farClip); // far clipping plane

            m_viewProj = m_viewMatrix * m_projectionMatrix;
        }

        // Moves the camera relative to the current camera position and target.
        protected void MoveCamera(Camera.cameraEnum p_direction, GameTime p_gametime, bool p_altSpeed)
        {
            Vector3 delta = Vector3.Zero;

            Matrix rot = Matrix.CreateFromYawPitchRoll(m_rotation.Y, m_rotation.X, 0);
            switch (p_direction)
            {
                case cameraEnum.FORWARD:
                    delta = Vector3.Transform(Vector3.Forward, rot);
                    break;
                case cameraEnum.BACK:
                    delta = Vector3.Transform(Vector3.Backward, rot);
                    break;
                case cameraEnum.LEFT:
                    delta = Vector3.Transform(Vector3.Left, rot);
                    break;
                case cameraEnum.RIGHT:
                    delta = Vector3.Transform(Vector3.Right, rot);
                    break;
                case cameraEnum.UP:
                    delta.Y = 1;
                    break;
                case cameraEnum.DOWN:
                    delta.Y = -1;
                    break;
            }

            float elapsedSeconds = ((float)(p_gametime.ElapsedRealTime.Milliseconds)) / 1000.0f;
            delta = Vector3.Multiply(delta, ((p_altSpeed) ? m_altSpeed : m_speed) * elapsedSeconds); // scale by camera speed

            // Update positon and lookAt
            m_position += delta;
            m_lookAt += delta;
        }

        private class ImmovableSkinPredicate : CollisionSkinPredicate1
        {
            public override bool ConsiderSkin(CollisionSkin skin0)
            {
                if (skin0.Owner != null) //&& !skin0.Owner.Immovable)
                    return true;
                else
                    return false;
            }
        }

        private void UpdateCameraFollowPos(GameTime p_gametime)
        {

            float frac; CollisionSkin skin;
            Vector3 pos, normal;

            ImmovableSkinPredicate pred = new ImmovableSkinPredicate();

            Vector3[] rays = new Vector3[5];

            //Up ray
            rays[0] = Vector3.Up;
            //Forward ray
            rays[1] = Vector3.Transform(Vector3.Right, Matrix.CreateRotationY(m_targetObject.rotation.Y));
            //Back ray
            rays[2] = Vector3.Transform(Vector3.Left, Matrix.CreateRotationY(m_targetObject.rotation.Y));
            //Left ray
            rays[3] = Vector3.Transform(Vector3.Forward, Matrix.CreateRotationY(m_targetObject.rotation.Y));
            //Right ray
            rays[4] = Vector3.Transform(Vector3.Backward, Matrix.CreateRotationY(m_targetObject.rotation.Y));

            float min_camera_distance = 3.0f;

            //Shoot all the rays and save the minimum distance to camera from any object hit
            for (int i = 0; i < 5; ++i)
            {
                ((Game1)Game).physSystem.CollisionSystem.SegmentIntersect(out frac, out skin, out pos, out normal,
                    new Segment(m_position, rays[i] * 1000.0f), pred);

                if (skin != null && (skin.Owner != null))
                {
                    GameObject objectHit = ((Game1)Game).gameWorld.GetObject(skin.Owner);
                    if (((m_position - pos).Length() < min_camera_distance)) //(objectHit.body.Immovable == true) && 
                    {
                        min_camera_distance = (m_position - pos).Length();                        
                    }
                }
            }
            
            float z_rot = -(3.0f - min_camera_distance) * 73.0f;
            if ((m_targetObject.position - m_position).Length() < 4)
            {
                z_rot += -(4 - (m_targetObject.position - m_position).Length()) * 30.0f;
            }
            z_rot = MathHelper.Clamp(z_rot, -73.0f, 0.0f);
            //Console.WriteLine((m_targetObject.position - m_position).Length());

            Vector3 delta_pos = Vector3.Transform(
                                 Vector3.Left * m_targetObject.gameModels[0].model.Meshes[0].BoundingSphere.Radius * 3.0f,
                                 Matrix.CreateRotationZ(MathHelper.ToRadians(-15 + z_rot)) *
                                 Matrix.CreateRotationY(m_targetObject.rotation.Y)) + m_targetObject.position - m_position;

            //If the camera is close to the car, move it back faster
            /*if ((m_targetObject.position - m_position).Length() < 10.0f)
            {
                m_position += delta_pos * (m_correction_factor * p_gametime.ElapsedGameTime.Milliseconds + 0.2f);
            }
            // If the camera is far from the car, move it forward faster
            else if ((m_targetObject.position - m_position).Length() > 13)
            {
                m_position += delta_pos * (m_correction_factor + 0.2f);
            }
            else
            {
                m_position += delta_pos * m_correction_factor * p_gametime.ElapsedGameTime.Milliseconds;
            }*/
            m_position += delta_pos * m_correction_factor * p_gametime.ElapsedGameTime.Milliseconds;
            //Console.WriteLine(p_gametime.ElapsedGameTime.Milliseconds);
        }

        public void UpdateCamera(GameTime p_gametime)
        {
            KeyboardState keyboardState = UserInput.instance.keyboardState;
            bool bShiftDown = (keyboardState.IsKeyDown(Keys.LeftShift) || keyboardState.IsKeyDown(Keys.RightShift));

            if (keyboardState.IsKeyDown(Keys.W))
            {
                MoveCamera(LloydGame.Camera.cameraEnum.FORWARD, p_gametime, bShiftDown);
            }
            if (keyboardState.IsKeyDown(Keys.S))
            {
                MoveCamera(LloydGame.Camera.cameraEnum.BACK, p_gametime, bShiftDown);
            }
            if (keyboardState.IsKeyDown(Keys.A))
            {
                MoveCamera(LloydGame.Camera.cameraEnum.LEFT, p_gametime, bShiftDown);
            }
            if (keyboardState.IsKeyDown(Keys.D))
            {
                MoveCamera(LloydGame.Camera.cameraEnum.RIGHT, p_gametime, bShiftDown);
            }
            if (keyboardState.IsKeyDown(Keys.L))
            {
                MoveCamera(LloydGame.Camera.cameraEnum.UP, p_gametime, bShiftDown);
            }
            if (keyboardState.IsKeyDown(Keys.K))
            {
                MoveCamera(LloydGame.Camera.cameraEnum.DOWN, p_gametime, bShiftDown);
            }

            // Determine how much the mouse has moved.
            MouseState currentMouseState = UserInput.instance.mouseState;
            MouseState previousMouseState = UserInput.instance.prevMouseState;

            switch (m_state)
            {
                case cameraState.FREE:
                    if (currentMouseState != previousMouseState)
                    {
                        float mouseDx = currentMouseState.X - ((Game1)Game).graphics.GraphicsDevice.Viewport.Width / 2;
                        float mouseDy = currentMouseState.Y - ((Game1)Game).graphics.GraphicsDevice.Viewport.Height / 2;

                        m_rotation.Y -= mouseDx * m_rotationSpeed;
                        m_rotation.Y = MathHelper.WrapAngle(m_rotation.Y);

                        m_rotation.X -= mouseDy * m_rotationSpeed;
                        m_rotation.X = MathHelper.Clamp(m_rotation.X,
                                                      -MathHelper.PiOver2 + 0.01f,
                                                      MathHelper.PiOver2 - 0.01f);

                        Mouse.SetPosition(((Game1)Game).graphics.GraphicsDevice.Viewport.Width / 2, ((Game1)Game).graphics.GraphicsDevice.Viewport.Height / 2);
                        previousMouseState = currentMouseState;
                    }
                    break;
                case cameraState.ROTATE_AROUND:
                    if (currentMouseState != previousMouseState)
                    {
                        float mouseDx = currentMouseState.X - ((Game1)Game).graphics.GraphicsDevice.Viewport.Width / 2;
                        float mouseDy = currentMouseState.Y - ((Game1)Game).graphics.GraphicsDevice.Viewport.Height / 2;

                        m_rotation.Y -= mouseDx * m_rotationSpeed;
                        m_rotation.Y = MathHelper.WrapAngle(m_rotation.Y);

                        m_rotation.X -= mouseDy * m_rotationSpeed;
                        m_rotation.X = MathHelper.WrapAngle(m_rotation.X);

                        Mouse.SetPosition(((Game1)Game).graphics.GraphicsDevice.Viewport.Width / 2, ((Game1)Game).graphics.GraphicsDevice.Viewport.Height / 2);
                        previousMouseState = currentMouseState;
                    }
                    break;
                case cameraState.EDIT_MODE:
                    if (currentMouseState.RightButton == ButtonState.Pressed &&
                        previousMouseState.RightButton == ButtonState.Pressed)
                    {
                        float mouseDx = currentMouseState.X - m_prevMousePosition.X;
                        float mouseDy = currentMouseState.Y - m_prevMousePosition.Y;

                        m_rotation.Y -= mouseDx * m_rotationSpeed;
                        m_rotation.Y = MathHelper.WrapAngle(m_rotation.Y);

                        m_rotation.X -= mouseDy * m_rotationSpeed;
                        m_rotation.X = MathHelper.WrapAngle(m_rotation.X);
                        m_prevMousePosition = new Vector2(currentMouseState.X, currentMouseState.Y);
                    }
                    if (currentMouseState.RightButton == ButtonState.Pressed &&
                        !(previousMouseState.RightButton == ButtonState.Pressed))
                    {
                        m_prevMousePosition = new Vector2(currentMouseState.X, currentMouseState.Y);
                    }
                    break;
            }

            switch (m_state)
            {
                case cameraState.FREE:
                    Matrix rot = Matrix.CreateFromYawPitchRoll(m_rotation.Y, m_rotation.X, 0);
                    m_lookAt = m_position + Vector3.Transform(Vector3.Forward, rot);
                    break;
                case cameraState.FOLLOW:
                    UpdateCameraFollowPos(p_gametime);
                    /*m_position = m_targetObject.position + Vector3.Transform(
                                 Vector3.Left * m_targetObject.gameModels[0].model.Meshes[0].BoundingSphere.Radius * 5,
                                 Matrix.CreateRotationZ(MathHelper.ToRadians(-15))* 
                                 Matrix.CreateRotationY(m_targetObject.rotation.Y));*/
                    m_lookAt = m_targetObject.position + Vector3.Up;
                    break;

                case cameraState.ROTATE_AROUND:
                    
                    Matrix rot__ = Matrix.CreateFromYawPitchRoll(m_rotation.Y, m_rotation.X, 0);
                    m_position = m_targetObject.position + Vector3.Transform(Vector3.Backward * m_rotationRadius, rot__);
                    m_lookAt = m_targetObject.position;
                    break;
                case cameraState.EDIT_MODE:
                    Matrix rotT = Matrix.CreateFromYawPitchRoll(m_rotation.Y, m_rotation.X, 0);
                    m_lookAt = m_position + Vector3.Transform(Vector3.Forward, rotT);
                    break;
            }


            m_viewMatrix = Matrix.CreateLookAt(m_position, m_lookAt, Vector3.Up);

            m_viewProj = m_viewMatrix * m_projectionMatrix;
        }
    }
}
